package ar.com.seminario.common.persistence.dao;

import java.io.Serializable;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import ar.com.seminario.common.domain.entity.Identifiable;
import ar.com.seminario.common.persistence.exception.DaoException;

/**
 * Clase genérica (DAO = Data Access Object) que provee las operaciones genéricas más comunes para
 * trabajar con clases persistentes. Este DAO puede ser extendido si hay operaciones específicas
 * requeridas sobre una clase persistente particular. Cabe destacar que todas las instancias de este
 * DAO deben obtenerse mediante el DaoFactory.
 * 
 * @author Barbalarga & Camarro & Costa Zini & Lamas & Lamas
 * @param <E> Tipo de instancias que manejar este DAO. Debe ser una clase persistente.
 * @param <Id> Tipo de identificador de la clase E.
 */
public class GenericDao<E extends Identifiable<Id>, Id extends Serializable> {

	/**
	 * Sesión de Hibernate con la cual trabajará el DAO genérico.
	 */
	private Session session;

	/**
	 * El constructor tiene un alcance protegido de modo que sea accedido sólo a nivel de subclases
	 * o paquete.
	 */
	protected GenericDao() {

	}

	/**
	 * Asigna la sesión de Hibernate que el DAO usar. Por defecto tiene alcance protegido, dado que
	 * el Factory es el único que debe setear esta propiedad.
	 * 
	 * @param session Sesión de Hibernate.
	 */
	protected final void setSession(final Session session) {

		this.session = session;
	}

	/**
	 * Retorna la sesión de Hibernate usada.
	 * 
	 * @return Sesión de Hibernate.
	 */
	public final Session getSession() {

		return (this.session);
	}

	/**
	 * Agrega un objeto a la sesión actual (mediante un Update).
	 * 
	 * @param <T> Tipo de entidad a obtener.
	 * @param instance La instancia a agregar.
	 */
	public final <T extends Identifiable<Id>> void attach(final T instance) {

		this.getSession().update(instance);
	}

	/**
	 * Delega al método 'delete' de la sesión de Hibernate.
	 * 
	 * @param instance Instancia a borrar.
	 * @throws DaoException En caso de problemas.
	 */
	public final void delete(final E instance) throws DaoException {

		try {
			this.getSession().delete(instance);
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Delega al método 'get' de la sesión de Hibernate.
	 * 
	 * @param clazz Clase de instancia a cargar.
	 * @param identifier Identificador del objeto a obtener.
	 * @return Objeto obtenido (o null si ninguno).
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final E get(final Class<E> clazz, final Id identifier) throws DaoException {

		try {
			return ((E) this.getSession().get(clazz, identifier));
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Delega al método 'load' de la sesión de Hibernate.
	 * 
	 * @param clazz Clase de instancia a cargar.
	 * @param identifier Identificador del objeto a obtener.
	 * @return Objeto obtenido (o null si ninguno).
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final E load(final Class<E> clazz, final Id identifier) throws DaoException {

		try {
			return ((E) this.getSession().load(clazz, identifier));
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Delega al método 'save' de la sesión de Hibernate.
	 * 
	 * @param instance Instancia a grabar.
	 * @return Identificador de la instancia grabada (útil si es nueva, con ID autogenerado).
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final Id save(final E instance) throws DaoException {

		try {
			return ((Id) this.getSession().save(instance));
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Delega al método 'saveOrUpdate' de la sesión de Hibernate.
	 * 
	 * @param instance Instancia a grabar.
	 * @throws DaoException En caso de problemas.
	 */
	public final void saveOrUpdate(final E instance) throws DaoException {

		try {
			this.getSession().saveOrUpdate(instance);
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Delega al método 'merge' de la sesión de Hibernate.
	 * 
	 * @param instance Instancia a mergear.
	 * @return Objeto mergeado.
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final E merge(final E instance) throws DaoException {

		try {
			return ((E) this.getSession().merge(instance));
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Verifica si la instancia de la clase indicada, con el identificador dado, existe o no en la
	 * base de datos.
	 * 
	 * @param clazz Clase de la instancia a verificar.
	 * @param identifier Identificador de la instancia.
	 * @return True si existe; caso contrario, False.
	 * @throws DaoException En caso de problemas.
	 */
	public final boolean exists(final Class<E> clazz, final Id identifier) throws DaoException {

		try {
			final Criteria criteria = this.getSession().createCriteria(clazz);
			criteria.add(Restrictions.idEq(identifier));
			criteria.setProjection(Projections.rowCount());

			criteria.setCacheable(true);

			return ((Integer) criteria.list().get(0)) > 0;

		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Obtiene una lista de todas las instancias de una clase específica en la base de datos.
	 * 
	 * @param clazz Clase.
	 * @return Todas las instancias en la base de datos.
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final List<E> getAll(final Class<E> clazz) throws DaoException {

		try {
			return (this.getSession().createCriteria(clazz).list());
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}


	/**
	 * Hace una búsqueda de una lista de resultados, para una Named Query, según los valores pasados
	 * como parámetros.
	 * 
	 * @param queryName Named query a ejecutar.
	 * @param parameters Los parámetros a pasarle como valores.
	 * @return Lista de resultados.
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final List<E> find(final String queryName, final QueryParameter... parameters)
	        throws DaoException {

		try {
			return (this.bindQuery(queryName, parameters).list());
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Hace una búsqueda de una lista de resultados, para una Named Query, según los valores pasados
	 * como parámetros. Obtiene los primeros N elementos solamente.
	 * 
	 * @param maxElements Cantidad máxima de elementos a buscar.
	 * @param queryName Named query a ejecutar.
	 * @param parameters Los parámetros a pasarle como valores.
	 * @return Lista de resultados.
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final List<E> findFirstElements(final int maxElements, final String queryName,
	        final QueryParameter... parameters) throws DaoException {

		try {
			final Query query = this.bindQuery(queryName, parameters);
			query.setMaxResults(maxElements);

			return query.list();

		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Obtiene un único resultado, para una Named Query, según los valores pasados como parámetros.
	 * 
	 * @param queryName Named query a ejecutar.
	 * @param parameters Los parámetros a pasarle como valores.
	 * @return Único resultado a obtener.
	 * @throws DaoException En caso de problemas.
	 */
	@SuppressWarnings("unchecked")
	public final E get(final String queryName, final QueryParameter... parameters)
	        throws DaoException {

		try {
			return ((E) this.bindQuery(queryName, parameters).uniqueResult());
		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}

	/**
	 * Arma una query a partir de una Named Query, con un conjunto de parámetros.
	 * 
	 * @param queryName Named query a usar.
	 * @param parameters Valores de parámetros a pasar.
	 * @return Query armada en Hibernate.
	 * @throws DaoException En caso de problemas.
	 */
	private Query bindQuery(final String queryName, final QueryParameter... parameters)
	        throws DaoException {

		try {
			final Query query = this.getSession().getNamedQuery(queryName);

			for (QueryParameter queryParameter : parameters) {
				query.setParameter(queryParameter.getName(), queryParameter.getValue());
			}

			return (query);

		} catch (final HibernateException e) {
			throw new DaoException(e);
		}
	}
}
